package com.webtest.fileupload;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.lang3.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.util.*;

/**
 * @author lzy
 * @version v1.0
 * Create DateTime: 2018/8/22 12:47
 * <p>
 * 获取请求信息:
 * 方法一：该方法无法获取到信息，因为编码方式已经改成二进制方式
 * String file = req.getParameter("file");
 * String desc = req.getParameter("desc");
 * <p>
 * LOGGER.info("File:" + file);
 * LOGGER.info("desc:" + desc);
 * <p>
 * 方法二：可以获取参数，但是非常麻烦，不建议采用该方法
 * InputStream inputStream = req.getInputStream();
 * BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
 * String str = null;
 * StringBuilder builder = new StringBuilder();
 * while ((str = reader.readLine()) != null) {
 * builder.append(str).append("\n");
 * }
 * LOGGER.info(builder.toString());
 * <p>
 * 方法三：采用commons-fileupload工具来实现
 */
public class UploadServlet extends HttpServlet {
    private static final long serialVersionUID = 4895471479511441444L;
    private static final Logger LOGGER = LoggerFactory.getLogger(UploadServlet.class);
    private FileInfoDao dao = new FileInfoDao();
    private String fileSavePath = FileUploadAppProperties.getInstance().getProperty("file.save.path");

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String methodName = req.getParameter("method");
        try {
            Method method = this.getClass().getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
            if (method != null) {
                method.invoke(this, req, resp);
            }
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }

    private void list(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        List<FileInfo> files = dao.getFileInfos();
        req.setAttribute("files", files);
        req.getRequestDispatcher("/fileupload/download.jsp").forward(req, resp);
    }


    private void download(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        String idstr = req.getParameter("id");
        long id = Long.parseLong(idstr);
        FileInfo fileInfo = dao.getFileInfo(id);

        resp.setContentType("application/x-msdownload");
        String fileName = fileInfo.getFileName();
        resp.setHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "utf-8"));

        OutputStream outputStream = resp.getOutputStream();
        String downloadFile = fileInfo.getFilePath();

        InputStream inputStream = new FileInputStream(downloadFile);
        byte[] buffer = new byte[1024];
        int len = 0;
        while ((len = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, len);
        }

        inputStream.close();
    }

    private void upload(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 1. 获取配置信息
        String fileMaxSize = FileUploadAppProperties.getInstance().getProperty("file.max.size");
        String totalFileSize = FileUploadAppProperties.getInstance().getProperty("total.file.size");

        /*
         * 采用commons-fileupload工具来实现
         */
        ServletFileUpload upload = getServletFileUpload(fileMaxSize, totalFileSize);
        String path = "";
        try {
            // 把需要上传的FileItem都放入到该Map中
            // key:文件待存放路径
            // value：
            Map<String, FileItem> uploadFiles = new HashMap<>(16);
            // 解析请求，得到FileItem的集合
            List<FileItem> items = upload.parseRequest(req);
            // 1.构建FileInfo集合,同时填充uploadFiles
            List<FileInfo> fileInfos = buildFileInfos(items, uploadFiles);

            // 2.校验扩展名
            validateExtName(fileInfos);

            // 3.校验文件的大小：在解析时已经校验了我们之需要通过异常得到结果

            // 4.进行文件的上传操作
            uploadFiles(uploadFiles);

            // 5.把上传的信息保存到数据库中
            dao.addFiles(fileInfos);
            path = "/fileupload/success.jsp";

            // 6.s删除临时文件夹下的临时文件
            deleteTempFiles(fileSavePath);
        } catch (FileUploadException | InvalidExtNameException | UnsupportedEncodingException e) {
            e.printStackTrace();
            path = "/fileupload/upload.jsp";
            req.setAttribute("message", e.getMessage());
        }
        req.getRequestDispatcher(path).forward(req, resp);
    }

    /**
     * 利用传入的FileItem集合，构建FileUploadBean的集合，同时填充FileInfo
     * <p>
     * FileInfo封装了上传文件的信息
     * uploadFiles：Map<String, FileItem>类型，存放文件域类型的FileItem，键：保存的文件的名字，值：FileItem
     * <p>
     * 构建过程：
     * 1.对传入的FileItem的集合进行遍历，得到desc的那个Map键，desc的fieldName(desc1,desc2,...)
     * 值：desc的那个输入文本值
     * 2.对传入的FileItem的集合进行遍历，得到文件域的那些FileItem对象，构建对应的key（desc1...）来获取其desc，构建FileInfo对象，并填充filePath和FileInfo
     *
     * @param items
     * @param uploadFiles
     * @return
     */
    private List<FileInfo> buildFileInfos(List<FileItem> items, Map<String, FileItem> uploadFiles) throws UnsupportedEncodingException {
        List<FileInfo> infos = new ArrayList<>();
        //1.遍历FileItem的集合，先得到desc的Map<String, String>,其中fieldName(desc1,desc2,...),
        // 值表单域对应字段的值
        Map<String, String> descs = new HashMap<>(16);
        for (FileItem item : items) {
            if (item.isFormField()) {
                descs.put(item.getFieldName(), item.getString("UTF-8"));
            }
        }

        //2.再遍历FileItem的集合，得到文件域的FileItem对象
        // 每个得到一个FileItem对象都创建一个FileInfo对象
        // 得到fileName, 构建filePath, 从1的Map中得到当前FileItem对应的那个desc
        // 使用fieldName后面的数字去匹配
        for (FileItem item : items) {
            if (!item.isFormField()) {
                String fieldName = item.getFieldName();
                String index = fieldName.substring(4);
                String fileName = item.getName();
                String desc = descs.get("desc" + index);
                if (desc != null) {
                    String filePath = getFilePath(fileName);
                    FileInfo info = new FileInfo(fileName, filePath, desc);
                    infos.add(info);
                    uploadFiles.put(filePath, item);
                }
            }
        }
        return infos;
    }

    /**
     * 根据给定的文件名构建一个随机的文件名
     * 1.构建的文件的文件名的扩展名和给定文件的扩展名一致
     * 2.利用ServletContext的getRealPath方法获取绝对路径
     * 3.利用Random和当前系统时间构建随机文件名
     *
     * @param fileName
     * @return
     */
    private String getFilePath(String fileName) {
        String extName = fileName.substring(fileName.lastIndexOf("."));
        long randNum = RandomUtils.nextLong(0, Long.MAX_VALUE);
        return fileSavePath + randNum + extName;
    }

    /**
     * 校验扩展名是否正确
     *
     * @param fileInfos
     */
    private void validateExtName(List<FileInfo> fileInfos) {
        String exts = FileUploadAppProperties.getInstance().getProperty("exts");
        List<String> extList = Arrays.asList(exts.split(","));
        for (FileInfo fileInfo : fileInfos) {
            String fileName = fileInfo.getFileName();
            String extName = fileName.substring(fileName.lastIndexOf(".") + 1);
            if (!extList.contains(extName)) {
                throw new InvalidExtNameException(fileName + "文件的扩展名不合法, 请上传" + exts + "类型的文件!");
            }
        }
    }

    /**
     * 将文件上传
     *
     * @param files
     */
    private void uploadFiles(Map<String, FileItem> files) {
        files.forEach(this::uploadFile);
    }

    private void uploadFile(String path, FileItem item) {
        try (InputStream inputStream = item.getInputStream();
             OutputStream outputStream = new FileOutputStream(path)) {
            byte[] buffer = new byte[1024];
            int len = 0;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            LOGGER.debug("upload:" + path);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 文件上传的io操作
     *
     * @param fileMaxSize
     * @param totalFileSize
     * @return
     */
    private ServletFileUpload getServletFileUpload(String fileMaxSize, String totalFileSize) {
        // 1.得到FileItem的集合items
        DiskFileItemFactory factory = new DiskFileItemFactory();

        // 设置内存中最多可以存放的上传文件的大小，若超出，则把文件写到一个临时文件夹里，以byte为单位
        factory.setSizeThreshold(1024 * 500);
        // 设置临时文件夹
        ServletContext servletContext = this.getServletConfig().getServletContext();
        File repository = new File(servletContext.getInitParameter("tempdir"));
        factory.setRepository(repository);
        ServletFileUpload upload = new ServletFileUpload(factory);

        // 设置最大上传文件总的大小
        upload.setSizeMax(Long.parseLong(totalFileSize));
        // 配置单个文件大小
        upload.setFileSizeMax(Long.parseLong(fileMaxSize));
        return upload;
    }

    private void deleteTempFiles(String filePath) {
        File file = new File(filePath);
        String[] files = file.list();
        if (files != null) {
            for (String f : files) {
                String extName = f.substring(f.lastIndexOf(".") + 1);
                if ("tmp".equals(extName)) {
                    File deleteFile = new File(filePath + f);
                    deleteFile.delete();
                }
            }
        }
    }
}
